--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Node / ReticulumProjects / MeshChatX.git / files / scripts / ci / github-upload-bunny-storage-release-assets.py
Displaying Raw • Download
scripts/ci/github-upload-bunny-storage-release-assets.py legacy/win7 (159b2358) Text, 5.39 KB
T8b949e#!/usr/bin/env python3
T8b949e"""Upload a directory tree to bunny.net Edge Storage (HTTP PUT per object)."""
Tff7b72from T7ee787__future__ Tff7b72import Te6edf3annotations
Tff7b72import T7ee787hashlib
Tff7b72import T7ee787json
Tff7b72import T7ee787mimetypes
Tff7b72import T7ee787os
Tff7b72import T7ee787sys
Tff7b72import T7ee787time
Tff7b72import T7ee787urllibT7ee787.T7ee787error
Tff7b72import T7ee787urllibT7ee787.T7ee787request
Tff7b72from T7ee787pathlib Tff7b72import Te6edf3Path
Tff7b72from T7ee787urllibT7ee787.T7ee787parse Tff7b72import Te6edf3quote
Tff7b72def Td2a8ffencode_object_relTb4b4b4(Te6edf3relTb4b4b4: Tffa657strTb4b4b4) Tff7b72-Tff7b72> Tffa657strTb4b4b4:
Tff7b72return Ta5d6ff"Ta5d6ff/Ta5d6ff"Tff7b72.Td2a8ffjoinTb4b4b4(Te6edf3quoteTb4b4b4(Te6edf3partTb4b4b4, Te6edf3safeTff7b72=Ta5d6ff"Ta5d6ff"Tb4b4b4) Tff7b72for Te6edf3part Tff7b72in Te6edf3relTff7b72.Td2a8ffsplitTb4b4b4(Ta5d6ff"Ta5d6ff/Ta5d6ff"Tb4b4b4)Tb4b4b4)
Tff7b72def Td2a8ffmime_forTb4b4b4(Te6edf3pathTb4b4b4: Te6edf3PathTb4b4b4) Tff7b72-Tff7b72> Tffa657strTb4b4b4:
Tff7b72if Te6edf3pathTff7b72.Td2a8ffsuffixTff7b72.Td2a8fflowerTb4b4b4(Tb4b4b4) Tff7b72== Ta5d6ff"Ta5d6ff.wasmTa5d6ff"Tb4b4b4:
Tff7b72return Ta5d6ff"Ta5d6ffapplication/wasmTa5d6ff"
Te6edf3guessedTb4b4b4, Te6edf3_enc Tff7b72= Te6edf3mimetypesTff7b72.Td2a8ffguess_typeTb4b4b4(Te6edf3pathTff7b72.Td2a8ffnameTb4b4b4)
Tff7b72return Te6edf3guessed Tff7b72or Ta5d6ff"Ta5d6ffapplication/octet-streamTa5d6ff"
Tff7b72def Td2a8ffget_jsonTb4b4b4(Te6edf3urlTb4b4b4: Tffa657strTb4b4b4, Te6edf3access_keyTb4b4b4: Tffa657strTb4b4b4, Te6edf3timeoutTb4b4b4: Tffa657int Tff7b72= T79c0ff120Tb4b4b4) Tff7b72-Tff7b72> Tffa657objectTb4b4b4:
Te6edf3req Tff7b72= Te6edf3urllibTff7b72.Td2a8ffrequestTff7b72.Td2a8ffRequestTb4b4b4(
Te6edf3urlTb4b4b4,
Te6edf3methodTff7b72=Ta5d6ff"Ta5d6ffGETTa5d6ff"Tb4b4b4,
Te6edf3headersTff7b72=Tb4b4b4{Ta5d6ff"Ta5d6ffAccessKeyTa5d6ff"Tb4b4b4: Te6edf3access_keyTb4b4b4}Tb4b4b4,
Tb4b4b4)
Tff7b72with Te6edf3urllibTff7b72.Td2a8ffrequestTff7b72.Td2a8ffurlopenTb4b4b4(Te6edf3reqTb4b4b4, Te6edf3timeoutTff7b72=Te6edf3timeoutTb4b4b4) Tff7b72as Te6edf3respTb4b4b4:
Te6edf3body Tff7b72= Te6edf3respTff7b72.Td2a8ffreadTb4b4b4(Tb4b4b4)
Tff7b72return Te6edf3jsonTff7b72.Td2a8ffloadsTb4b4b4(Te6edf3bodyTff7b72.Td2a8ffdecodeTb4b4b4(Ta5d6ff"Ta5d6ffutf-8Ta5d6ff"Tb4b4b4)Tb4b4b4)
Tff7b72def Td2a8ffdelete_pathTb4b4b4(Te6edf3urlTb4b4b4: Tffa657strTb4b4b4, Te6edf3access_keyTb4b4b4: Tffa657strTb4b4b4, Te6edf3timeoutTb4b4b4: Tffa657int Tff7b72= T79c0ff120Tb4b4b4) Tff7b72-Tff7b72> Tff7b72NoneTb4b4b4:
Te6edf3req Tff7b72= Te6edf3urllibTff7b72.Td2a8ffrequestTff7b72.Td2a8ffRequestTb4b4b4(
Te6edf3urlTb4b4b4,
Te6edf3methodTff7b72=Ta5d6ff"Ta5d6ffDELETETa5d6ff"Tb4b4b4,
Te6edf3headersTff7b72=Tb4b4b4{Ta5d6ff"Ta5d6ffAccessKeyTa5d6ff"Tb4b4b4: Te6edf3access_keyTb4b4b4}Tb4b4b4,
Tb4b4b4)
Tff7b72tryTb4b4b4:
Tff7b72with Te6edf3urllibTff7b72.Td2a8ffrequestTff7b72.Td2a8ffurlopenTb4b4b4(Te6edf3reqTb4b4b4, Te6edf3timeoutTff7b72=Te6edf3timeoutTb4b4b4) Tff7b72as Te6edf3respTb4b4b4:
Te6edf3code Tff7b72= Te6edf3respTff7b72.Td2a8ffgetcodeTb4b4b4(Tb4b4b4)
Tff7b72except Te6edf3urllibTff7b72.Td2a8fferrorTff7b72.Td2a8ffHTTPError Tff7b72as Te6edf3eTb4b4b4:
Te6edf3code Tff7b72= Te6edf3eTff7b72.Td2a8ffcode
Tff7b72if Te6edf3code Tff7b72== T79c0ff404Tb4b4b4:
Tff7b72return
Te6edf3eTff7b72.Td2a8ffreadTb4b4b4(T79c0ff500Tb4b4b4)
Tff7b72raise Tf85149SystemExitTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffHTTP Tffd700{Te6edf3codeTffd700}Ta5d6ff DELETE Tffd700{Te6edf3urlTffd700}Ta5d6ff"Tb4b4b4) Tff7b72from T7ee787e
Tff7b72elseTb4b4b4:
Tff7b72if Te6edf3code Tff7b72not Tff7b72in Tb4b4b4(T79c0ff200Tb4b4b4, T79c0ff201Tb4b4b4, T79c0ff204Tb4b4b4)Tb4b4b4:
Tff7b72raise Tf85149SystemExitTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffunexpected DELETE status Tffd700{Te6edf3codeTffd700}Ta5d6ff for Tffd700{Te6edf3urlTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72def Td2a8ffprune_other_versionsTb4b4b4(
Te6edf3baseTb4b4b4: Tffa657strTb4b4b4,
Te6edf3access_keyTb4b4b4: Tffa657strTb4b4b4,
Te6edf3trackTb4b4b4: Tffa657strTb4b4b4,
Te6edf3keep_versionTb4b4b4: Tffa657strTb4b4b4,
Tb4b4b4) Tff7b72-Tff7b72> Tff7b72NoneTb4b4b4:
T8b949e"""Remove every version directory under ``track/`` except ``keep_version``."""
Te6edf3base Tff7b72= Te6edf3baseTff7b72.Td2a8ffrstripTb4b4b4(Ta5d6ff"Ta5d6ff/Ta5d6ff"Tb4b4b4)
Te6edf3list_url Tff7b72= Ta5d6fffTa5d6ff"Tffd700{Te6edf3baseTffd700}Ta5d6ff/Tffd700{Te6edf3encode_object_relTb4b4b4(Te6edf3trackTb4b4b4)Tffd700}Ta5d6ff/Ta5d6ff"
Tff7b72tryTb4b4b4:
Te6edf3listing Tff7b72= Te6edf3get_jsonTb4b4b4(Te6edf3list_urlTb4b4b4, Te6edf3access_keyTb4b4b4)
Tff7b72except Te6edf3urllibTff7b72.Td2a8fferrorTff7b72.Td2a8ffHTTPError Tff7b72as Te6edf3eTb4b4b4:
Tff7b72if Te6edf3eTff7b72.Td2a8ffcode Tff7b72== T79c0ff404Tb4b4b4:
Tff7b72return
Tff7b72raise
Tff7b72if Tff7b72not Tffa657isinstanceTb4b4b4(Te6edf3listingTb4b4b4, Tffa657listTb4b4b4)Tb4b4b4:
Tff7b72return
Te6edf3seenTb4b4b4: Tffa657setTb4b4b4[Tffa657strTb4b4b4] Tff7b72= Tffa657setTb4b4b4(Tb4b4b4)
Tff7b72for Te6edf3item Tff7b72in Te6edf3listingTb4b4b4:
Tff7b72if Tff7b72not Tffa657isinstanceTb4b4b4(Te6edf3itemTb4b4b4, Tffa657dictTb4b4b4)Tb4b4b4:
Tff7b72continue
Tff7b72if Tff7b72not Te6edf3itemTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffIsDirectoryTa5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72continue
Te6edf3name Tff7b72= Te6edf3itemTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffObjectNameTa5d6ff"Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3name Tff7b72or Tff7b72not Tffa657isinstanceTb4b4b4(Te6edf3nameTb4b4b4, Tffa657strTb4b4b4)Tb4b4b4:
Tff7b72continue
Tff7b72if Te6edf3name Tff7b72in Te6edf3seenTb4b4b4:
Tff7b72continue
Te6edf3seenTff7b72.Td2a8ffaddTb4b4b4(Te6edf3nameTb4b4b4)
Tff7b72if Te6edf3name Tff7b72== Te6edf3keep_versionTb4b4b4:
Tff7b72continue
Te6edf3rel Tff7b72= Ta5d6fffTa5d6ff"Tffd700{Te6edf3trackTffd700}Ta5d6ff/Tffd700{Te6edf3nameTffd700}Ta5d6ff"
Te6edf3delete_url Tff7b72= Ta5d6fffTa5d6ff"Tffd700{Te6edf3baseTffd700}Ta5d6ff/Tffd700{Te6edf3encode_object_relTb4b4b4(Te6edf3relTb4b4b4)Tffd700}Ta5d6ff"
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffprune: DELETE Tffd700{Te6edf3delete_urlTffd700}Ta5d6ff"Tb4b4b4, Te6edf3fileTff7b72=Te6edf3sysTff7b72.Td2a8ffstderrTb4b4b4)
Te6edf3delete_pathTb4b4b4(Te6edf3delete_urlTb4b4b4, Te6edf3access_keyTb4b4b4)
Tff7b72def Td2a8ffput_fileTb4b4b4(
Te6edf3urlTb4b4b4: Tffa657strTb4b4b4,
Te6edf3bodyTb4b4b4: Tffa657bytesTb4b4b4,
Te6edf3access_keyTb4b4b4: Tffa657strTb4b4b4,
Te6edf3content_typeTb4b4b4: Tffa657strTb4b4b4,
Te6edf3max_attemptsTb4b4b4: Tffa657int Tff7b72= T79c0ff4Tb4b4b4,
Tb4b4b4) Tff7b72-Tff7b72> Tff7b72NoneTb4b4b4:
Te6edf3checksum Tff7b72= Te6edf3hashlibTff7b72.Td2a8ffsha256Tb4b4b4(Te6edf3bodyTb4b4b4)Tff7b72.Td2a8ffhexdigestTb4b4b4(Tb4b4b4)Tff7b72.Td2a8ffupperTb4b4b4(Tb4b4b4)
Te6edf3timeout Tff7b72= T79c0ff600 Tff7b72if Tffa657lenTb4b4b4(Te6edf3bodyTb4b4b4) Tff7b72> T79c0ff50_000_000 Tff7b72else T79c0ff120
Tff7b72for Te6edf3attempt Tff7b72in Tffa657rangeTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3max_attempts Tff7b72+ T79c0ff1Tb4b4b4)Tb4b4b4:
Te6edf3req Tff7b72= Te6edf3urllibTff7b72.Td2a8ffrequestTff7b72.Td2a8ffRequestTb4b4b4(
Te6edf3urlTb4b4b4,
Te6edf3dataTff7b72=Te6edf3bodyTb4b4b4,
Te6edf3methodTff7b72=Ta5d6ff"Ta5d6ffPUTTa5d6ff"Tb4b4b4,
Te6edf3headersTff7b72=Tb4b4b4{
Ta5d6ff"Ta5d6ffAccessKeyTa5d6ff"Tb4b4b4: Te6edf3access_keyTb4b4b4,
Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4: Te6edf3content_typeTb4b4b4,
Ta5d6ff"Ta5d6ffChecksumTa5d6ff"Tb4b4b4: Te6edf3checksumTb4b4b4,
Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tff7b72tryTb4b4b4:
Tff7b72with Te6edf3urllibTff7b72.Td2a8ffrequestTff7b72.Td2a8ffurlopenTb4b4b4(Te6edf3reqTb4b4b4, Te6edf3timeoutTff7b72=Te6edf3timeoutTb4b4b4) Tff7b72as Te6edf3respTb4b4b4:
Te6edf3code Tff7b72= Te6edf3respTff7b72.Td2a8ffgetcodeTb4b4b4(Tb4b4b4)
Tff7b72except Te6edf3urllibTff7b72.Td2a8fferrorTff7b72.Td2a8ffHTTPError Tff7b72as Te6edf3eTb4b4b4:
Te6edf3code Tff7b72= Te6edf3eTff7b72.Td2a8ffcode
Te6edf3err_body Tff7b72= Te6edf3eTff7b72.Td2a8ffreadTb4b4b4(T79c0ff500Tb4b4b4)
Tff7b72if Te6edf3code Tff7b72in Tb4b4b4(T79c0ff200Tb4b4b4, T79c0ff201Tb4b4b4)Tb4b4b4:
Tff7b72return
Tff7b72if T79c0ff500 Tff7b72<Tff7b72= Te6edf3code Tff7b72< T79c0ff600 Tff7b72and Te6edf3attempt Tff7b72< Te6edf3max_attemptsTb4b4b4:
Te6edf3timeTff7b72.Td2a8ffsleepTb4b4b4(T79c0ff0.5 Tff7b72* Tb4b4b4(T79c0ff2 Tff7b72*Tff7b72* Tb4b4b4(Te6edf3attempt Tff7b72- T79c0ff1Tb4b4b4)Tb4b4b4)Tb4b4b4)
Tff7b72continue
Tff7b72raise Tf85149SystemExitTb4b4b4(
Ta5d6fffTa5d6ff"Ta5d6ffHTTP Tffd700{Te6edf3codeTffd700}Ta5d6ff for Tffd700{Te6edf3urlTffd700}Ta5d6ff: Tffd700{Te6edf3err_bodyTffd700!r}Ta5d6ff"Tb4b4b4,
Tb4b4b4) Tff7b72from T7ee787e
Tff7b72except Tb4b4b4(Te6edf3urllibTff7b72.Td2a8fferrorTff7b72.Td2a8ffURLErrorTb4b4b4, Tf85149TimeoutErrorTb4b4b4) Tff7b72as Te6edf3eTb4b4b4:
Tff7b72if Te6edf3attempt Tff7b72< Te6edf3max_attemptsTb4b4b4:
Te6edf3timeTff7b72.Td2a8ffsleepTb4b4b4(T79c0ff0.5 Tff7b72* Tb4b4b4(T79c0ff2 Tff7b72*Tff7b72* Tb4b4b4(Te6edf3attempt Tff7b72- T79c0ff1Tb4b4b4)Tb4b4b4)Tb4b4b4)
Tff7b72continue
Tff7b72raise Tf85149SystemExitTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffrequest failed for Tffd700{Te6edf3urlTffd700}Ta5d6ff: Tffd700{Te6edf3eTffd700}Ta5d6ff"Tb4b4b4) Tff7b72from T7ee787e
Tff7b72elseTb4b4b4:
Tff7b72if Te6edf3code Tff7b72in Tb4b4b4(T79c0ff200Tb4b4b4, T79c0ff201Tb4b4b4)Tb4b4b4:
Tff7b72return
Tff7b72raise Tf85149SystemExitTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffunexpected status Tffd700{Te6edf3codeTffd700}Ta5d6ff for Tffd700{Te6edf3urlTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72def Td2a8ffmainTb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tff7b72NoneTb4b4b4:
Te6edf3base Tff7b72= Te6edf3osTff7b72.Td2a8ffenvironTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffBUNNY_STORAGE_BASE_URLTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffrstripTb4b4b4(Ta5d6ff"Ta5d6ff/Ta5d6ff"Tb4b4b4)
Te6edf3key Tff7b72= Te6edf3osTff7b72.Td2a8ffenvironTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffBUNNY_STORAGE_ACCESS_KEYTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff"Tb4b4b4)
Te6edf3prefix Tff7b72= Te6edf3osTff7b72.Td2a8ffenvironTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffBUNNY_STORAGE_OBJECT_PREFIXTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Ta5d6ff"Ta5d6ff/Ta5d6ff"Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3base Tff7b72or Tff7b72not Te6edf3keyTb4b4b4:
Tffa657printTb4b4b4(
Ta5d6ff"Ta5d6ffBUNNY_STORAGE_BASE_URL and BUNNY_STORAGE_ACCESS_KEY must be setTa5d6ff"Tb4b4b4,
Te6edf3fileTff7b72=Te6edf3sysTff7b72.Td2a8ffstderrTb4b4b4,
Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Tff7b72if Te6edf3prefixTb4b4b4:
Te6edf3seg Tff7b72= Te6edf3prefixTff7b72.Td2a8ffsplitTb4b4b4(Ta5d6ff"Ta5d6ff/Ta5d6ff"Tb4b4b4, T79c0ff1Tb4b4b4)
Tff7b72if Tffa657lenTb4b4b4(Te6edf3segTb4b4b4) Tff7b72== T79c0ff2 Tff7b72and Te6edf3segTb4b4b4[T79c0ff0Tb4b4b4] Tff7b72in Tb4b4b4(Ta5d6ff"Ta5d6ffmasterTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffdevTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3prune_other_versionsTb4b4b4(Te6edf3baseTb4b4b4, Te6edf3keyTb4b4b4, Te6edf3segTb4b4b4[T79c0ff0Tb4b4b4]Tb4b4b4, Te6edf3segTb4b4b4[T79c0ff1Tb4b4b4]Tb4b4b4)
Te6edf3root Tff7b72= Te6edf3PathTb4b4b4(Te6edf3sysTff7b72.Td2a8ffargvTb4b4b4[T79c0ff1Tb4b4b4]Tb4b4b4)Tff7b72.Td2a8ffresolveTb4b4b4(Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3rootTff7b72.Td2a8ffis_dirTb4b4b4(Tb4b4b4)Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffnot a directory: Tffd700{Te6edf3rootTffd700}Ta5d6ff"Tb4b4b4, Te6edf3fileTff7b72=Te6edf3sysTff7b72.Td2a8ffstderrTb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Te6edf3files Tff7b72= Tffa657sortedTb4b4b4(Te6edf3p Tff7b72for Te6edf3p Tff7b72in Te6edf3rootTff7b72.Td2a8ffrglobTb4b4b4(Ta5d6ff"Ta5d6ff*Ta5d6ff"Tb4b4b4) Tff7b72if Te6edf3pTff7b72.Td2a8ffis_fileTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3filesTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffno files under Tffd700{Te6edf3rootTffd700}Ta5d6ff"Tb4b4b4, Te6edf3fileTff7b72=Te6edf3sysTff7b72.Td2a8ffstderrTb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Tff7b72for Te6edf3path Tff7b72in Te6edf3filesTb4b4b4:
Te6edf3rel Tff7b72= Te6edf3pathTff7b72.Td2a8ffrelative_toTb4b4b4(Te6edf3rootTb4b4b4)Tff7b72.Td2a8ffas_posixTb4b4b4(Tb4b4b4)
Tff7b72if Te6edf3prefixTb4b4b4:
Te6edf3object_rel Tff7b72= Ta5d6fffTa5d6ff"Tffd700{Te6edf3prefixTffd700}Ta5d6ff/Tffd700{Te6edf3relTffd700}Ta5d6ff"
Tff7b72elseTb4b4b4:
Te6edf3object_rel Tff7b72= Te6edf3rel
Te6edf3url Tff7b72= Ta5d6fffTa5d6ff"Tffd700{Te6edf3baseTffd700}Ta5d6ff/Tffd700{Te6edf3encode_object_relTb4b4b4(Te6edf3object_relTb4b4b4)Tffd700}Ta5d6ff"
Te6edf3body Tff7b72= Te6edf3pathTff7b72.Td2a8ffread_bytesTb4b4b4(Tb4b4b4)
Te6edf3put_fileTb4b4b4(Te6edf3urlTb4b4b4, Te6edf3bodyTb4b4b4, Te6edf3keyTb4b4b4, Te6edf3mime_forTb4b4b4(Te6edf3pathTb4b4b4)Tb4b4b4)
Tffa657printTb4b4b4(Te6edf3urlTb4b4b4)
Tff7b72if Tff7b72__name__ Tff7b72== Ta5d6ff"Ta5d6ff__main__Ta5d6ff"Tb4b4b4:
Te6edf3mainTb4b4b4(Tb4b4b4)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────